
我们在前面章节中开发的tinylang编译器，但没有对IR代码进行优化。接下来的小节中，我们将在编译器中添加一个优化流水线来实现这一目标。

\mySubsubsection{7.5.1.}{创建优化流水线}

PassBuilder类是设置优化流水线的核心。这个类知道所有已注册的通道，并且可以从文本描述构造一个通道流水线。可以使用这个类根据命令行给出的描述创建通道流水线，或者根据所请求的优化级别使用默认流水线。还支持使用通道插件，例如：在前一节中讨论的ppprofiler通道插件，就可以模拟opt工具的部分功能，并为命令行选项使用类似的名称。

PassBuilder类填充ModulePassManager类的实例，ModulePassManager类是通道管理器，保存构造的通道流水线并运行，代码生成通道仍然使用旧的通道管理器，所以必须保留旧的通道管理器。

为了实现它，我们将扩展tinylang编译器中的tools/driver/Driver.cpp文件:

\begin{enumerate}
\item
这里将使用新的类，所以将从添加新的包含文件开始。llvm/Passes/PassBuilder.h文件定义了PassBuilder类，llvm/Passes/PassPlugin.h文件是插件支持所必需的。最后，llvm/Analysis/targettransformminfo.h文件提供了一个IR级转换，并与目标特定信息连接起来的通道:

\begin{cpp}
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Analysis/TargetTransformInfo.h"
\end{cpp}

\item
要使用新通道管理器的某些特性，必须添加三个命令行选项，使用与opt工具相同的名称。-{}-passes选项允许通过管道的文本规范，而-{}-load-pass-plugin选项允许使用通道插件。若给出了-{}-debug-pass-manager选项，则通道管理器会输出执行通道的信息:

\begin{cpp}
static cl::opt<bool>
    DebugPM("debug-pass-manager", cl::Hidden,
        cl::desc("Print PM debugging information"));
static cl::opt<std::string> PassPipeline(
    "passes",
    cl::desc("A description of the pass pipeline"));
static cl::list<std::string> PassPlugins(
    "load-pass-plugin",
    cl::desc("Load passes from plugin library"));
\end{cpp}

\item
用户以优化级别影响通道流水线的构造。PassBuilder类支持6个不同的优化级别:无优化、3个优化速度级别和2个减小文件大小级别。我们可以在一个命令行选项中获取所有级别:

\begin{cpp}
static cl::opt<signed char> OptLevel(
    cl::desc("Setting the optimization level:"),
    cl::ZeroOrMore,
    cl::values(
        clEnumValN(3, "O", "Equivalent to -O3"),
        clEnumValN(0, "O0", "Optimization level 0"),
        clEnumValN(1, "O1", "Optimization level 1"),
        clEnumValN(2, "O2", "Optimization level 2"),
        clEnumValN(3, "O3", "Optimization level 3"),
        clEnumValN(-1, "Os", "Like -O2 with extra optimizations for size"),
        clEnumValN(-2, "Oz", "Like -Os but reduces code size further")),
        cl::init(0));
\end{cpp}

\item
LLVM的插件机制支持静态链接插件的插件注册表，是在项目配置期间创建的。为了利用这个注册表，必须包含llvm/Support/Extension.def数据库文件，以创建返回插件信息的函数原型:

\begin{cpp}
#define HANDLE_EXTENSION(Ext) \
    llvm::PassPluginLibraryInfo get##Ext##PluginInfo();
#include "llvm/Support/Extension.def"
\end{cpp}

\item
现在，必须用一个新版本替换现有的emit()函数，必须在函数的顶部声明所需的PassBuilder实例:

\begin{cpp}
bool emit(StringRef Argv0, llvm::Module *M,
          llvm::TargetMachine *TM,
          StringRef InputFilename) {
    PassBuilder PB(TM);
\end{cpp}

\item
为了实现对命令行中给出的传递插件的支持，必须循环遍历用户给出的插件库列表，并尝试加载插件。若此操作失败，将输出一个错误消息;否则，将注册通道:

\begin{cpp}
for (auto &PluginFN : PassPlugins) {
    auto PassPlugin = PassPlugin::Load(PluginFN);
    if (!PassPlugin) {
        WithColor::error(errs(), Argv0)
            << "Failed to load passes from '" << PluginFN
            << "'. Request ignored.\n";
        continue;
    }

    PassPlugin->registerPassBuilderCallbacks(PB);
}
\end{cpp}

\item
来自静态插件注册表的信息，以类似的方式用于向PassBuilder实例注册这些插件:

\begin{cpp}
#define HANDLE_EXTENSION(Ext) \
    get##Ext##PluginInfo().RegisterPassBuilderCallbacks(PB);
#include "llvm/Support/Extension.def"
\end{cpp}

\item
现在，需要为不同的分析管理器声明变量。唯一的参数是调试标志:

\begin{cpp}
LoopAnalysisManager LAM(DebugPM);
FunctionAnalysisManager FAM(DebugPM);
CGSCCAnalysisManager CGAM(DebugPM);
ModuleAnalysisManager MAM(DebugPM);
\end{cpp}

\item
接下来，必须使用对PassBuilder实例上各自的注册方法的调用来填充分析管理器。分析管理器将使用默认分析通道填充，并运行注册回调。还必须确保功能分析管理器使用默认别名分析流水线，并且所有的分析管理器都知道彼此的存在:

\begin{cpp}
FAM.registerPass(
    [&] { return PB.buildDefaultAAPipeline(); });
PB.registerModuleAnalyses(MAM);
PB.registerCGSCCAnalyses(CGAM);
PB.registerFunctionAnalyses(FAM);
PB.registerLoopAnalyses(LAM);
PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);
\end{cpp}

\item
MPM模块通道管理器保存构造的通道流水线，实例用debug标志初始化:

\begin{cpp}
ModulePassManager MPM(DebugPM);
\end{cpp}

\item
现在，需要实现两种不同的方法，用通道流水线填充模块通道管理器。若用户在命令行上提供了一个通道流水线——使用了-{}-passes选项——则使用这个作为通道流水线:

\begin{cpp}
if (!PassPipeline.empty()) {
    if (auto Err = PB.parsePassPipeline(
    MPM, PassPipeline)) {
        WithColor::error(errs(), Argv0)
        << toString(std::move(Err)) << "\n";
        return false;
    }
}
\end{cpp}

\item
否则，使用所选择的优化级别来确定要构建的通道流水线。默认的通道流水线名称为default，将优化级别作为参数:

\begin{cpp}
else {
    StringRef DefaultPass;
    switch (OptLevel) {
        case 0: DefaultPass = "default<O0>"; break;
        case 1: DefaultPass = "default<O1>"; break;
        case 2: DefaultPass = "default<O2>"; break;
        case 3: DefaultPass = "default<O3>"; break;
        case -1: DefaultPass = "default<Os>"; break;
        case -2: DefaultPass = "default<Oz>"; break;
    }
    if (auto Err = PB.parsePassPipeline(
            MPM, DefaultPass)) {
        WithColor::error(errs(), Argv0)
        << toString(std::move(Err)) << "\n";
        return false;
    }
}
\end{cpp}


\item
IR代码上运行转换的通道流水线就已经设置好了。之后，需要一个打开的文件来写入结果。系统汇编器和LLVM IR输出是基于文本的，所以应该为其设置OF\_Text标志:

\begin{cpp}
std::error_code EC;
sys::fs::OpenFlags OpenFlags = sys::fs::OF_None;
CodeGenFileType FileType = codegen::getFileType();
if (FileType == CGFT_AssemblyFile)
    OpenFlags |= sys::fs::OF_Text;
auto Out = std::make_unique<llvm::ToolOutputFile>(
    outputFilename(InputFilename), EC, OpenFlags);
if (EC) {
    WithColor::error(errs(), Argv0)
        << EC.message() << '\n';
    return false;
}
\end{cpp}

\item
对于代码生成过程，必须使用旧的通道管理器。必须简单地声明CodeGenPM实例并添加通道，这使得特定于目标的信息在进行IR级别的转换时可用:

\begin{cpp}
legacy::PassManager CodeGenPM;
CodeGenPM.add(createTargetTransformInfoWrapperPass(
    TM->getTargetIRAnalysis()));
\end{cpp}

\item
要输出LLVM IR，必须添加一个将IR输出到流中的通道:

\begin{cpp}
if (FileType == CGFT_AssemblyFile && EmitLLVM) {
    CodeGenPM.add(createPrintModulePass(Out->os()));
}
\end{cpp}

\item
否则，必须让TargetMachine实例添加所需的代码生成通道，由作为参数传递的FileType值作为指示:

\begin{cpp}
else {
    if (TM->addPassesToEmitFile(CodeGenPM, Out->os(),
                                nullptr, FileType)) {
        WithColor::error()
        << "No support for file type\n";
        return false;
    }
}
\end{cpp}

\item
所有这些准备工作之后，准备执行通道。必须在IR模块上运行优化流水线，再运行代码生成通道。在所有这些工作之后，保留输出文件:

\begin{cpp}
    MPM.run(*M, MAM);
    CodeGenPM.run(*M);
    Out->keep();
    return true;
}
\end{cpp}

\item
虽然代码很多，但过程很简单。我们也必须更新tools/driver/CMakeLists.txt构建文件中的依赖项，除了添加目标组件，还必须添加来自LLVM的所有转换和代码生成组件，这些名称大致类似于源代码所在的目录名称。在配置过程中，组件名会转换为链接库名:

\begin{cmake}
set(LLVM_LINK_COMPONENTS ${LLVM_TARGETS_TO_BUILD}
    AggressiveInstCombine Analysis AsmParser
    BitWriter CodeGen Core Coroutines IPO IRReader
    InstCombine Instrumentation MC ObjCARCOpts Remarks
    ScalarOpts Support Target TransformUtils Vectorize
    Passes)
\end{cmake}

\item
编译器驱动支持插件，必须声明这个支持:

\begin{cmake}
target_link_libraries(tinylang
    PRIVATE tinylangBasic tinylangCodeGen
    tinylangLexer tinylangParser tinylangSema)
\end{cmake}

这些都是对源代码和构建系统的必要补充。

\item
要构建扩展编译器，必须切换到构建目录并键入以下命令:

\begin{shell}
$ ninja
\end{shell}

\end{enumerate}

对构建系统文件的更改会自动检测，并且在编译和链接更改的源代码前运行cmake。若需要重新运行配置步骤，请按照第1章，编译tinylang应用程序部分中的说明进行操作。

我们已经使用了opt工具的选项作为蓝图，所以应该尝试运行tinylang，使用这些选项来加载一个通道插件并运行通道。

当前的实现中，既可以运行默认的传递管道，也可以自己构造一个。后者是非常灵活的，但它几乎是多余的。默认管道在类C语言中运行得非常好，但缺少的是一种扩展通道流水线的方法。我们将在下一节中讨论如何实现它。

\mySubsubsection{7.5.2.}{扩展通道流水线}

上一节中，使用PassBuilder类从用户提供的描述或预定义的名称创建了一个通道流水线。现在，让我们看看定制通道流水线的另一种方法:扩展点。

通道流水线的构造过程中，通道构建器允许添加用户提供的通道，这些点称为扩展点。一些扩展点的特特征，如下所示:

\begin{itemize}
\item
流水线起始扩展点，允许在流水线的开头添加通道

\item
窥视孔扩展点，允许在每个指令组合器实例通道之后添加通道
\end{itemize}

还存在其他扩展点。要使用扩展点，必须注册回调。通道流水线的构造过程中，回调在定义的扩展点上运行，并且可以向给定的通道管理器添加通道。

要为流水线的起始扩展点注册回调函数，必须调用PassBuilder类的registerPipelineStartEPCallback()方法。例如，要在流水线的开头添加PPProfiler通道，需要调用createModuleToFunctionPassAdaptor()模板函数，将该通道修改为模块通道，然后将其添加到模块通道管理器:

\begin{cpp}
PB.registerPipelineStartEPCallback(
    [](ModulePassManager &MPM) {
        MPM.addPass(PPProfilerIRPass());
    });
\end{cpp}

可以在流水线创建之前的任何地方——也就是parsepaspipeline()方法调用之前，在通道流水线设置代码中添加这段代码。

对于上一节中所做的工作，一个非常自然的扩展是让用户在命令行上添加管道扩展点的流水线描述。opt工具也可以这样做，让我们对管道开始扩展点执行此操作。在tools/driver/Driver.cpp文件中添加如下代码:

\begin{enumerate}
\item
首先，必须为用户创建一个新的命令行来指定管道描述，从opt工具中获取选项名:

\begin{cpp}
static cl::opt<std::string> PipelineStartEPPipeline(
    "passes-ep-pipeline-start",
    cl::desc("Pipeline start extension point));
\end{cpp}

\item
使用Lambda函数作为回调是最方便的方式。为了解析管道描述，必须调用PassBuilder实例的parsepaspipeline()方法。这些通道会添加到PM通道管理器中，并作为Lambda函数的参数。若发生错误，只打印错误消息，而不停止应用程序。在调用crossRegisterProxies()方法后，添加以下代码:

\begin{cpp}
PB.registerPipelineStartEPCallback(
[&PB, Argv0](ModulePassManager &PM) {
    if (auto Err = PB.parsePassPipeline(
            PM, PipelineStartEPPipeline)) {
        WithColor::error(errs(), Argv0)
            << "Could not parse pipeline "
            << PipelineStartEPPipeline.ArgStr << ": "
            << toString(std::move(Err)) << "\n";
    }
});
\end{cpp}

\begin{myTip}{Tip}
要允许用户在每个扩展点添加通道，需要为每个扩展点添加上述代码片段。
\end{myTip}

\item
是时候尝试不同的传递管理器选项了。使用-{}-debugpass-manager选项，可以了解哪些通道以何种顺序执行。还可以在每个通道执行前后打印IR，通过-{}-print-before-all和-{}-print-after-all选项完成。若创建了自己的通道流水线，就可以在感兴趣的地方插入打印通道。例如，可以尝试-{}-passes="print,inline,print"选项。此外，为了识别哪一个通道改变了IR代码，可以使用-{}-printchanged选项，只会在IR代码与之前通道的结果相比发生了变化时打印出来，大大减少的输出使得跟踪IR修改变得更加容易。

PassBuilder类有一个嵌套的OptimizationLevel类来表示六个不同的优化级别。而不是使用“default<O?”>"流水描述作为parsePassPipeline()方法的参数，也可以调用buildPerModuleDefaultPipeline()方法，为请求级别构建默认的优化管道——除了级别0，此优化级别不执行任何优化。

没有通道添加到通道管理器中，若仍然想要运行某个通道，则可以手动将其添加到通道管理器中。这个级别上运行的一个简单通道是AlwaysInliner通道，将一个带有always\_inline属性的函数内联到调用者中，将优化级别的命令行选项值转换为OptimizationLevel类的相应成员之后，可以这样实现:

\begin{cpp}
PassBuilder::OptimizationLevel Olevel = …;
if (OLevel == PassBuilder::OptimizationLevel::O0)
    MPM.addPass(AlwaysInlinerPass());
else
    MPM = PB.buildPerModuleDefaultPipeline(OLevel, DebugPM);
\end{cpp}

当然，可以以这种方式向通道管理器添加多个通道。构造通道流水线时，PassBuilder也使用addPass()方法。

\end{enumerate}

\begin{myTip}{运行扩展点的回调}
因为没有为优化级别O0填充通道流水线，所以注册的扩展点不会调用。使用扩展点来注册应该在O0级别运行的通道会有问题，可以使用runRegisteredEPCallbacks()来运行注册的扩展点回调，从而产生一个仅由通过扩展点注册通道填充的通道管理器。
\end{myTip}

通过将优化流水线添加到tinylang中，既创建了一个类似于clang的优化编译器。LLVM社区致力于在每个版本中改进优化和优化管道，所以很少不使用默认管道。大多数情况下，添加新通道是为了实现编程语言的某些语义。





































